Skip to content

Normalize single element convolution arg lists - #22778

Open
giuliocorradi wants to merge 1 commit into
pytorch:mainfrom
giuliocorradi:vulkan-conv-valid-padding
Open

giuliocorradi wants to merge 1 commit into
pytorch:mainfrom
giuliocorradi:vulkan-conv-valid-padding

Conversation

@giuliocorradi

@giuliocorradi giuliocorradi commented Sep 13, 2026

Copy link
Copy Markdown

Fixes #22774

Summary

ATen lets a convolution's stride, padding, dilation and output_padding be given as a single value that applies to every spatial dimension, and torch.nn does exactly that for padding="valid", which exports as padding=[0] rather than [0, 0].

The Vulkan convolution reads these as fixed-width vectors — make_ivec2_from_listmake_ivec2, which requires exactly two elements — so a 2D convolution written that way lowers fine and then aborts at the first inference:

make_ivec2 at backends/vulkan/runtime/utils/VecUtils.h:367:
  (ints.size() == 2) is false!

The identical convolution written padding=0 runs correctly, so only the spelling of the argument decided whether the model worked.

This matters beyond one op: padding="valid" is how HuggingFace writes SigLIP and CLIP patch embeddings —

self.patch_embedding = nn.Conv2d(
    in_channels=config.num_channels,
    out_channels=self.embed_dim,
    kernel_size=self.patch_size,
    stride=self.patch_size,
    padding="valid",
)

— so every vision tower of that shape hit it, with an assert deep in the runtime that gives no hint that padding spelling is the cause.

The change

A NormalizeConvolutionArgs pass that broadcasts a single-element list to the number of spatial dimensions, run at the start of the preprocess pipeline where the graph is still ATen compliant. A 1D convolution is left alone, since one element already matches its one spatial dim.

On where to fix it: broadcasting in make_ivec2_from_list would also be reasonable and is smaller. I did it in the graph because the normalized graph is what every later pass and the serializer see, and because I can test a Python change against a stock runtime — I have no build of the Vulkan runtime here, so a C++ patch would have gone out unverified. Happy to move it if you prefer the runtime.

Test plan

python -m unittest backends.vulkan.test.test_vulkan_passes -v

12 tests pass, including the new test_normalize_convolution_args_broadcasts_single_element_lists, which asserts a 2D convolution's [0] padding becomes [0, 0] and a 1D convolution's stays [0].

Verified end to end on an AMD Radeon 8060S (RADV GFX1151, RDNA 3.5), lowering each module and comparing the delegate's output against eager:

case before after
Conv2d(padding=0) ok ok (unchanged)
Conv2d(padding=1) ok ok (unchanged)
Conv2d(padding=(1, 1)) ok ok (unchanged)
Conv2d(padding="valid") make_ivec2 abort ok, max|d| 6.0e-07
SigLIP patch embed, k=14 s=14, "valid" make_ivec2 abort ok, max|d| 1.2e-06
Conv1d(padding=1) ok ok (unchanged)
Conv1d(padding="valid") ok ok (unchanged)

backends.vulkan.test.test_vulkan_delegate produces an identical set of results before and after (its failures in my environment are pre-existing and unrelated).

Found while lowering openpi's π₀.₅ — whose vision tower is SigLIP — to the Vulkan delegate.

cc @SS-JIA @manuelcandales @digantdesai @cbilgin

ATen lets a convolution's stride, padding, dilation and output_padding be
given as a single value that applies to every spatial dimension, and
torch.nn does exactly that for `padding="valid"`, which exports as
`padding=[0]` rather than `[0, 0]`.

The Vulkan convolution reads these as fixed width vectors
(make_ivec2_from_list -> make_ivec2, which requires exactly 2 elements),
so a 2D convolution written that way aborted at the first inference:

  make_ivec2 ... (ints.size() == 2) is false!

The same convolution written `padding=0` lowers and runs correctly, so
only the spelling of the argument decided whether the model worked.

Add a NormalizeConvolutionArgs pass that broadcasts a single element list
to the number of spatial dimensions, and run it at the start of the
preprocess pipeline, where the graph is still ATen compliant. A 1D
convolution is left alone, since one element already matches its one
spatial dim.

This is worth doing in the graph rather than the runtime because the
normalized graph is what every later pass and the serializer see, but
broadcasting in make_ivec2_from_list instead would also be a reasonable
fix.

`padding="valid"` is how HuggingFace writes SigLIP and CLIP patch
embeddings, so every vision tower of that shape was affected.

Test Plan:

  python -m unittest backends.vulkan.test.test_vulkan_passes -v

12 tests pass, including the new
test_normalize_convolution_args_broadcasts_single_element_lists, which
checks that a 2D convolution's `[0]` padding becomes `[0, 0]` and that a
1D convolution's stays `[0]`.

Verified end to end on an AMD Radeon 8060S (RADV GFX1151), lowering each
module and comparing the delegate's output against eager:

  case                                   before          after
  Conv2d(padding=0)                      ok              ok
  Conv2d(padding=1)                      ok              ok
  Conv2d(padding=(1, 1))                 ok              ok
  Conv2d(padding="valid")                make_ivec2      ok, max|d| 6.0e-07
  SigLIP patch embed, k=14 s=14, valid   make_ivec2      ok, max|d| 1.2e-06
  Conv1d(padding=1)                      ok              ok
  Conv1d(padding="valid")                ok              ok

backends.vulkan.test.test_vulkan_delegate shows an identical set of
results before and after the change.
Copilot AI lite review requested due to automatic review settings September 13, 2026 03:41
@pytorch-bot

pytorch-bot Bot commented Sep 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22778

Note: Links to docs will display an error until the docs builds have been completed.

❗ 1 Active SEVs

There are 1 currently active SEVs. If your PR is affected, please view them below:

❌ 1 Awaiting Approval, 1 Unclassified Failure

As of commit 8f947f2 with merge base 903696a (image):

AWAITING APPROVAL - The following workflow needs approval before CI can run:

UNCLASSIFIED FAILURE - DrCI could not classify the following job because the workflow did not run on the merge base. The failure may be pre-existing on trunk or introduced by this PR:

  • Cadence Build & Test / Resolve CI docker image / resolve (gh) (this job did not run on the merge base, so DrCI cannot tell whether the failure is pre-existing)
    ##[error]Refusing to check out fork pull request code from a 'pull_request_target' workflow. This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch cache scope, and runner access. Fetching and executing a fork's code in that trusted context commonly leads to "pwn request" vulnerabilities. To opt in, review the risks at https://gh.io/securely-using-pull_request_target and set 'allow-unsafe-pr-checkout: true' on the actions/checkout step.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla

meta-cla Bot commented Sep 13, 2026

Copy link
Copy Markdown

Hi @giuliocorradi!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@linux-foundation-easycla

Copy link
Copy Markdown

CLA Not Signed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The fix is narrowly scoped to Vulkan preprocessing, addresses the reported crash path, and includes a targeted regression test for the primary failing case.

Pull request overview

This PR fixes a Vulkan runtime abort triggered by ATen-compliant convolutions whose stride/padding/dilation/output_padding are exported as single-element lists (notably nn.Conv2d(padding="valid") exporting padding=[0]), by normalizing those argument lists early in the Vulkan preprocess pipeline.

Changes:

  • Add a Vulkan preprocessing pass (NormalizeConvolutionArgs) that broadcasts single-element convolution argument lists to the convolution’s spatial rank (leaving 1D conv unchanged).
  • Run the new normalization pass at the start of the Vulkan preprocess pass pipeline.
  • Add a unit test covering the padding="valid" export case (2D broadcast) and ensuring 1D conv remains unmodified.
File summaries
File Description
backends/vulkan/vulkan_preprocess.py Inserts the new normalization pass early in the Vulkan preprocess pipeline to keep later stages/runtime inputs consistent.
backends/vulkan/_passes/normalize_convolution_args.py Implements broadcasting of single-element convolution argument lists based on inferred spatial rank.
backends/vulkan/_passes/init.py Exposes NormalizeConvolutionArgs via the Vulkan passes package.
backends/vulkan/test/test_vulkan_passes.py Adds a regression test for padding="valid" exporting as a one-element list and verifies 2D broadcast / 1D no-op behavior.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +825 to +845
# 2D: one element padding must be broadcast to the two spatial dims.
edge_program = to_edge(
torch.export.export(Conv2dValid().eval(), (torch.randn(1, 3, 32, 32),)),
compile_config=EdgeCompileConfig(_check_ir_validity=False),
)
ep = edge_program._edge_programs["forward"]
self.assertEqual(padding_of(ep), [0], "expected export to emit a 1D padding")

ep = _transform(ep, NormalizeConvolutionArgs())
self.assertEqual(padding_of(ep), [0, 0])

# 1D: a one element list already matches the single spatial dim, so it
# must be left alone.
edge_program = to_edge(
torch.export.export(Conv1dValid().eval(), (torch.randn(1, 3, 32),)),
compile_config=EdgeCompileConfig(_check_ir_validity=False),
)
ep = _transform(
edge_program._edge_programs["forward"], NormalizeConvolutionArgs()
)
self.assertEqual(padding_of(ep), [0])

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f947f27ed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +784 to +788
"""A 2D convolution written `padding="valid"` exports a one element
padding list, which the Vulkan convolution cannot read.

make_ivec2_from_list -> make_ivec2 requires exactly 2 elements, so such
a convolution aborted at the first inference with

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Trim the duplicated test narrative

This lengthy docstring repeats the implementation rationale, runtime assertion, and downstream model context rather than concisely describing the behavior under test; the assertions below already make the 2-D broadcast and 1-D no-op expectations clear. Keeping the same explanation in both the pass and its test increases maintenance burden, so reduce this to a short behavioral description.

AGENTS.md reference: AGENTS.md:L46-L50

Useful? React with 👍 / 👎.

@nil-is-all nil-is-all added module: vulkan Issues related to the Vulkan delegate and code under backends/vulkan/ release notes: vulkan Changes to the Vulkan backend delegate labels Sep 16, 2026
@executorch-triage executorch-triage Bot added the community: contribution PRs coming from community (excluding hardware partners) label Sep 22, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community: contribution PRs coming from community (excluding hardware partners) module: vulkan Issues related to the Vulkan delegate and code under backends/vulkan/ release notes: vulkan Changes to the Vulkan backend delegate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ET-VK] Conv2d with padding="valid" asserts in make_ivec2 (breaks every SigLIP/CLIP patch embed)

4 participants